---
title: Astro 适配器 API
---

import Since from '~/components/Since.astro';
import { FileTree } from '@astrojs/starlight/components';

Astro 可以轻松部署到任何云托管平台，以实现服务端渲染（SSR）。该能力由**适配器**[集成](/zh-cn/reference/integrations-reference/)提供，请参阅 [SSR 指南](/zh-cn/guides/server-side-rendering/) 了解如何使用现有的适配器。

## 什么是适配器

适配器是一种特殊类型的[集成](/zh-cn/reference/integrations-reference/)，它为服务器端渲染提供了入口。适配器包含两项主要功能：

- 实现托管平台的 API，以处理请求。
- 根据托管平台的约定配置构建过程。

## 构建适配器

由于适配器是一种[集成](/zh-cn/reference/integrations-reference/)，因此它拥有集成提供的全部能力。

__必须__ 通过在 `astro:config:done` 钩子中调用 `setAdapter` API 来使用适配器，例如：

```js title="my-adapter.mjs"
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          supportedAstroFeatures: {
              staticOutput: 'stable'
          }  
        });
      },
    },
  };
}
```

`setAdapter` 的传入参数定义如下：

```ts
interface AstroAdapter {
	name: string;
	serverEntrypoint?: string;
	previewEntrypoint?: string;
	exports?: string[];
	args?: any;
	adapterFeatures?: AstroAdapterFeatures;
	supportedAstroFeatures?: AstroFeatureMap;
}

export interface AstroAdapterFeatures {
	/**
   * 创建一个与 Astro 中间件通信的边缘函数
	 */
	edgeMiddleware: boolean;
	/**
   * 仅 SSR 可用。每个路由都会成为自己的函数/文件
	 */
	functionPerRoute: boolean;
}

export type SupportsKind = 'unsupported' | 'stable' | 'experimental' | 'deprecated';

export type AstroFeatureMap = {
  /**
   * 该适配器是否能够为静态页面提供服务
   */
  staticOutput?: SupportsKind;
  /**
   * 该适配器是否能够为静态页面或通过服务器渲染的页面提供服务。
   */
  hybridOutput?: SupportsKind;
  /**
   * 此适配器是否能够为 SSR 页面提供服务
   */
  serverOutput?: SupportsKind;
  /**
   * 此适配器对 assets 的支持程度
   */
  assets?: AstroAssetsFeature;
};

export interface AstroAssetsFeature {
  supportKind?: SupportsKind;
  /**
   * 此适配器是否在与库 `sharp` 兼容的环境中部署文件
   */
  isSharpCompatible?: boolean;
  /**
   * 此适配器是否在与库 `squoosh` 兼容的环境中部署文件
   */
  isSquooshCompatible?: boolean;
}

```

这些属性分别是：

* __name__：适配器的唯一名称，用于日志记录。
* __serverEntrypoint__：服务端渲染的入口。
* __exports__：导出数组，与 `createExports` 配套使用（在下文中说明）。
* __adapterFeatures__：一个对象，用于启用适配器必须支持的特定功能。这些功能将改变构建输出，适配器必须实现适当的逻辑来处理不同的输出。
* __supportedAstroFeatures__：Astro 内置功能的映射。这允许 Astro 确定适配器无法或不愿意支持的功能，以便提供适当的错误消息。

### 服务端入口

Astro 的适配器 API 尝试适配多种类型的托管方，并提供了灵活的配置方式。

#### Exports 

一些无服务架构的托管方会希望你导出一个`handler`函数：

```js
export function handler(event, context) {
  // ...
}
```

在适配器 API 中，你可以在 `serverEntrypoint` 中实现 `createExports` 方法：

```js
import { App } from 'astro/app';

export function createExports(manifest) {
  const app = new App(manifest);

  const handler = (event, context) => {
    // ...
  };

  return { handler };
}
```

在此之后，你需要在 `setAdapter` 的 `exports` 属性中配置该 `handler`：

```js title="my-adapter.mjs" ins={9}
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          exports: ['handler'],
        });
      },
    },
  };
}
```

#### Start 

有些托管方希望你自行管理服务的**启动**，例如通过监听一个端口的方式。对于这类托管方，可以导出一个 `start` 函数，该函数会在绑定脚本执行时被调用。

```js
import { App } from 'astro/app';

export function start(manifest) {
  const app = new App(manifest);

  addEventListener('fetch', event => {
    // ...
  });
}
```

#### `astro/app`

该模块用于渲染已通过 `astro build` 命令预构建的页面。Astro 使用标准的 [Request](https://developer.mozilla.org/zh-CN/docs/Web/API/Request) 和 [Response](https://developer.mozilla.org/zh-CN/docs/Web/API/Response) 对象。如果托管方使用不同格式的请求/响应 API，需要在适配器中进行转换处理。

```js
import { App } from 'astro/app';
import http from 'http';

export function start(manifest) {
  const app = new App(manifest);

  addEventListener('fetch', event => {
    event.respondWith(
      app.render(event.request)
    );
  });
}
```

该模块提供以下几个方法：

##### `app.render(request: Request, options?: RenderOptions)`

此方法用于匹配符合请求的 Astro 页面，并返回一个 Promise 对象给 [Response](https://developer.mozilla.org/zh-CN/docs/Web/API/Response) 。该方法对于不渲染页面的 API 路由同样适用。

```js
const response = await app.render(request);
```

##### `RenderOptions`

`app.render()` 方法接受一个必填的 `request` 参数，以及一个可选的 `RenderOptions` 对象，用于 [`addCookieHeader`](#addcookieheader)、[`clientAddress`](#clientaddress)、[`locals`](#locals) 和 [`routeData`](#routedata)。

###### `addCookieHeader`

是否自动将 `Astro.cookie.set()` 写入的所有 cookie 添加到响应头中。

当设置为 `true` 时，它们将作为逗号分隔的键值对添加到响应的 `Set-Cookie` 头中。你可以使用标准的 `response.headers.getSetCookie()` API 来单独读取它们。
当设置为 `false`（默认值）时，这些 cookie 只能从 `App.getSetCookieFromResponse(response)` 中获取。

```js
const response = await app.render(request, { addCookieHeader: true });
```

###### `clientAddress`

该客户端 IP 地址将作为 [`Astro.clientAddress`](/zh-cn/reference/api-reference/#astroclientaddress) 在页面中可用，并作为 API 路由和中间件中的 `ctx.clientAddress`。

下面的示例读取 `x-forwarded-for` 头，并将其作为 `clientAddress` 传递。该值将作为 `Astro.clientAddress` 提供给用户。

```js "clientAddress"
const clientAddress = request.headers.get("x-forwarded-for");
const response = await app.render(request, { clientAddress });
```

###### `locals`

[`context.locals` 对象](/zh-cn/reference/api-reference/#contextlocals) 用于在请求的生命周期中存储和访问信息。

下面的示例读取名为 `x-private-header` 的头，并尝试将其解析为对象并将其传递给 `locals`，然后可以将其传递给任何 [中间件函数](/zh-cn/guides/middleware/)。

```js "locals"
const privateHeader = request.headers.get("x-private-header");
let locals = {};
try {
    if (privateHeader) {
        locals = JSON.parse(privateHeader);
    }
} finally {
    const response = await app.render(request, { locals });
}
```

###### `routeData`

如果你已经知道要渲染的路由，请为 [`routeData`](/zh-cn/reference/integrations-reference/#routedata-类型参考) 提供一个值。这样做将绕过内部调用 [`app.match`](#appmatchrequest) 来确定要渲染的路由。

```js "routeData"
const routeData = app.match(request);
if (routeData) {
    return app.render(request, { routeData });
} else {
    /* 特定于适配器的 404 响应 */
    return new Response(..., { status: 404 });
}
```

##### `app.match(request)`

该方法用于判断请求是否匹配 Astro 应用的路由规则。

```js
if(app.match(request)) {
  const response = await app.render(request);
}
```

通常可以在不使用 `.match` 的情况下调用 `app.render(request)`。因为当配置了 `404.astro` 文件后，Astro 就会自动处理 404 的情况。如果想要自定义处理规则，请使用 `app.match(request)`。

## 使用 `astro add` 安装适配器

用户可以使用 [`astro add` 命令](/zh-cn/reference/cli-reference/#astro-add) 轻松地在他们的项目中添加集成和适配器。如果希望其他用户可以使用该命令安装 _你的_ 适配器，**请在 `package.json` 文件的 `keywords` 项中添加 `astro-adapter` 属性**：

```json
{
  "name": "example",
  "keywords": ["astro-adapter"],
}
```

当[将适配器发布到 npm](https://docs.npmjs.com/cli/v8/commands/npm-publish) 后，执行 `astro add example` 命令，即可安装适配器以及在 `package.json` 文件中指定的对等依赖。我们将指导用户手动更新他们的项目配置。



## Astro features

<p><Since v="3.0.0" /></p>

Astro features 是适配器告诉 Astro 它们是否能够支持某个特性的一种方式，也是适配器支持程度的一种方式。

当使用这些属性时，Astro 将：
- 运行特定的验证；
- 抛出（emit）上下文日志；

这些操作是基于支持或不支持的特性、支持程度以及用户使用的配置来运行的。

以下配置告诉 Astro，该适配器对 assets 有实验性支持，但该适配器与内置服务 Sharp 和 Squoosh 不兼容：

```js title="my-adapter.mjs" ins={9-15}
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          supportedAstroFeatures: {
            assets: {
              supportKind: "experimental",
              isSharpCompatible: false,
              isSquooshCompatible: false
            }
          }  
        });
      },
    },
  };
}
```

Astro 将在终端中记录**警告**：

```
/* 该功能是实验性的，可能会出现问题或更改。*/
[@matthewp/my-adapter] The feature is experimental and subject to issues or changes.
```

或者，如果用于 assets 的服务与适配器不兼容，则会抛出错误：

```
/* 当前选择的适配器 `@matthewp/my-adapter` 与 "Sharp" 服务不兼容。你的项目将无法构建。*/
[@matthewp/my-adapter] The currently selected adapter `@matthewp/my-adapter` is not compatible with the service "Sharp". Your project will NOT be able to build.
```

## Adapter features

一组可以改变产出文件输出的特性。当适配器选择这些特性时，它们将在特定的钩子中获得额外的信息。

### `functionPerRoute`

这是一个仅在使用 SSR 时启用的功能。默认情况下，Astro 会产出一个 `entry.mjs` 文件，该文件负责在每个请求上产出渲染的页面。

当 `functionPerRoute` 为 `true` 时，Astro 会为项目中定义的每个路由创建一个单独的文件。

每个文件都只会渲染一个页面。页面将在 `dist/pages/` 目录下（或者在 [`outDir`](/zh-cn/reference/configuration-reference/#outdir) 所指定目录中的 `/pages/` 目录下）产出，产出的文件将保持与 `src/pages/` 目录相同的文件路径。

构建出的 `pages/` 目录下的文件，将会与 `src/pages/` 目录下的页面文件的目录结构保持一致，例如：

<FileTree>
- dist/
    - pages/
        - blog/
            - entry.\_slug\_.astro.mjs
            - entry.about.astro.mjs
        - entry.index.astro.mjs
</FileTree>

通过将 `true` 传递给适配器来启用此功能。

```js title="my-adapter.mjs" ins={9-11}
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          adapterFeatures: {
              functionPerRoute: true
          } 
        });
      },
    },
  };
}
```

然后，使用 [`astro:build:ssr`](/zh-cn/reference/integrations-reference/#astrobuildssr) 钩子，它将为你提供一个 `entryPoints` 对象，该对象将页面路由映射到构建后的物理文件。

```js title="my-adapter.mjs" ins={15-19}
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          adapterFeatures: {
              functionPerRoute: true
          } 
        });
      },

      'astro:build:ssr': ({ entryPoints }) => {
          for (const [route, entryFile] of entryPoints) {
              // 对路由和条目文档执行某些操作
          }
      }  
    },
  };
}
```

:::caution
`entryFile` 是 `URL` 类型，表示文件系统中文件的物理路径。这意味着路径会根据代码运行的操作系统而改变。
:::

#### 无服务器（Serverless）环境

在无服务器（Serverless）环境中设置 `functionPerRoute: true` 会为每个路由创建一个 JavaScript 文件（handler）。根据你的托管平台，处理程序可能会有不同的名称：lambda、function、page 等。

每个路由都会在处理程序（handler）运行时受到 [冷启动](https://azure.microsoft.com/en-us/blog/understanding-serverless-cold-start/) 的影响，这可能会导致一些延迟。这种延迟受到不同因素的影响。

当设置 `functionPerRoute: false` 时，只有一个单一的处理程序（handler）负责渲染所有路由。当此处理程序（handler）首次触发时，你将受到冷启动的影响。然后，所有其他路由都应该没有延迟。但是，你将无法享受由 `functionPerRoute: true` 提供的代码拆分（code splitting）所带来的好处。

:::note
了解你的托管平台以及它的工作方式，是选择适合你的项目的 `functionPerRoute` 配置的重要前提。
:::

### `edgeMiddleware`

定义在构建时是否会打包任何 SSR 中间件代码。

启用此功能时，会阻止在构建期间将中间件代码打包并导入到所有页面中：

```js title="my-adapter.mjs" ins={9-11}
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          adapterFeatures: {
              edgeMiddleware: true
          } 
        });
      },
    },
  };
}
```

然后，使用 [`astro:build:ssr`](/zh-cn/reference/integrations-reference/#astrobuildssr) 钩子，它将为你提供一个 `middlewareEntryPoint`，一个指向文件系统上物理文件的 `URL`。

```js title="my-adapter.mjs" ins={15-19}
export default function createIntegration() {
  return {
    name: '@matthewp/my-adapter',
    hooks: {
      'astro:config:done': ({ setAdapter }) => {
        setAdapter({
          name: '@matthewp/my-adapter',
          serverEntrypoint: '@matthewp/my-adapter/server.js',
          adapterFeatures: {
              edgeMiddleware: true
          } 
        });
      },

      'astro:build:ssr': ({ middlewareEntryPoint }) => {
          // 请记住检查此属性是否退出，如果适配器未选择加入该功能，则它将是 `undefined`
          if (middlewareEntryPoint) {
             createEdgeMiddleware(middlewareEntryPoint)
          }
      }  
    },
  };
}

function createEdgeMiddleware(middlewareEntryPoint) {
    // 通过你的打包工具生成一个新的物理文件
}
```
